// TODO: CODE REVIEW & CLEANUP!
//
// TouchKeyboard.cs
//
// An on-screen keyboard.
//

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using HomeUX.ClientControlSupport;

namespace HomeUX.Controls
{

/// <summary>
/// 
/// </summary>
/// 
/// <remarks>
/// Known limitations:
/// <list type="bullet">
///     <item>
///         While the Shift button is lit, arrowing left and right shortens and lengthens the selection, as expected,
///         However, if you left-arrow past the beginning of the selection you might expect the selection to begin again
///         in the opposite direction, but instead the insertion point simply moves to the left.  This is due to a
///         Silverlight limitation (no programmatic way to determine or manipulate the insertion point within a
///         selection in a <n>TextBox</n>).
///     </item>
/// </list>
/// </remarks>
[TemplatePart(Name = LayoutRootName, Type = typeof(Panel))]
[TemplatePart(Name = DisabledVisualElementName, Type = typeof(UIElement))]
[TemplatePart(Name = BackspaceButtonName, Type = typeof(ButtonBase))]
[TemplatePart(Name = TabButtonName, Type = typeof(ButtonBase))]
[TemplatePart(Name = CapsLockButtonName, Type = typeof(TouchButton))]
[TemplatePart(Name = EnterButtonName, Type = typeof(TouchButton))]
[TemplatePart(Name = LeftShiftButtonName, Type = typeof(TouchButton))]
[TemplatePart(Name = RightShiftButtonName, Type = typeof(TouchButton))]
[TemplatePart(Name = SpaceButtonName, Type = typeof(ButtonBase))]
[TemplatePart(Name = DeleteButtonName, Type = typeof(ButtonBase))]
[TemplatePart(Name = HomeButtonName, Type = typeof(ButtonBase))]
[TemplatePart(Name = LeftArrowButtonName, Type = typeof(ButtonBase))]
[TemplatePart(Name = RightArrowButtonName, Type = typeof(ButtonBase))]
[TemplatePart(Name = EndButtonName, Type = typeof(ButtonBase))]
[TemplateVisualState(GroupName = CommonStatesName, Name = NormalStateName)]
[TemplateVisualState(GroupName = CommonStatesName, Name = DisabledStateName)]
[TemplateVisualState(GroupName = DisabledHelpStatesName, Name = ShowDisabledHelpStateName)]
[TemplateVisualState(GroupName = DisabledHelpStatesName, Name = HideDisabledHelpStateName)]
public class TouchKeyboard : Control, IControlHelperConnector
{
    const string LayoutRootName = "LayoutRoot";
    const string DisabledVisualElementName = "DisabledVisualElement";
    const string BackspaceButtonName = "BackspaceButton";
    const string TabButtonName = "TabButton";
    const string CapsLockButtonName = "CapsLockButton";
    const string EnterButtonName = "EnterButton";
    const string LeftShiftButtonName = "LeftShiftButton";
    const string RightShiftButtonName = "RightShiftButton";
    const string SpaceButtonName = "SpaceButton";
    const string DeleteButtonName = "DeleteButton";
    const string HomeButtonName = "HomeButton";
    const string LeftArrowButtonName = "LeftArrowButton";
    const string RightArrowButtonName = "RightArrowButton";
    const string EndButtonName = "EndButton";

    const string CommonStatesName = "CommonStates";
    const string NormalStateName = "Normal";
    const string DisabledStateName = "Disabled";
    const string DisabledHelpStatesName = "DisabledHelpStates";
    const string ShowDisabledHelpStateName = "ShowDisabledHelp";
    const string HideDisabledHelpStateName = "HideDisabledHelp";

    Panel _layoutRoot;
    UIElement _disabledVisualElement;
    ButtonBase _backspaceButton;
    ButtonBase _tabButton;
    TouchButton _capsLockButton;
    TouchButton _enterButton;
    TouchButton _leftShiftButton;
    TouchButton _rightShiftButton;
    ButtonBase _spaceButton;
    ButtonBase _deleteButton;
    ButtonBase _homeButton;
    ButtonBase _leftArrowButton;
    ButtonBase _rightArrowButton;
    ButtonBase _endButton;

    bool _isCapsLocked;
    bool _isShifted;
    bool _isDisabledHelpDisplayed;

    TextBox _targetTextBox;

    DispatcherTimer _focusPollTimer;

    public string TabScope { get; set; }

    public event RoutedEventHandler EnterClick;

    public TouchKeyboard()
    {
        // specify the default style
        base.DefaultStyleKey = typeof(TouchKeyboard);
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        ControlHelper.GetRequiredTemplateChild(this, LayoutRootName, out _layoutRoot);
        ControlHelper.GetRequiredTemplateChild(this, DisabledVisualElementName, out _disabledVisualElement);
        ControlHelper.GetRequiredTemplateChild(this, BackspaceButtonName, out _backspaceButton);
        ControlHelper.GetRequiredTemplateChild(this, TabButtonName, out _tabButton);
        ControlHelper.GetRequiredTemplateChild(this, CapsLockButtonName, out _capsLockButton);
        ControlHelper.GetRequiredTemplateChild(this, EnterButtonName, out _enterButton);
        ControlHelper.GetRequiredTemplateChild(this, LeftShiftButtonName, out _leftShiftButton);
        ControlHelper.GetRequiredTemplateChild(this, RightShiftButtonName, out _rightShiftButton);

        ControlHelper.GetRequiredTemplateChild(this, SpaceButtonName, out _spaceButton);
        ControlHelper.GetRequiredTemplateChild(this, DeleteButtonName, out _deleteButton);
        ControlHelper.GetRequiredTemplateChild(this, HomeButtonName, out _homeButton);
        ControlHelper.GetRequiredTemplateChild(this, LeftArrowButtonName, out _leftArrowButton);
        ControlHelper.GetRequiredTemplateChild(this, RightArrowButtonName, out _rightArrowButton);
        ControlHelper.GetRequiredTemplateChild(this, EndButtonName, out _endButton);
        
        _disabledVisualElement.MouseLeftButtonDown += _disabledVisualElement_MouseLeftButtonDown;
        _backspaceButton.Click += BackspaceButton_Click;
        _tabButton.Click += TabButton_Click;
        _capsLockButton.Click += CapsLockButton_Click;
        _enterButton.Click += EnterButton_Click;
        _leftShiftButton.Click += ShiftButton_Click;
        _rightShiftButton.Click += ShiftButton_Click;
        _spaceButton.Click += SpaceButton_Click;
        _deleteButton.Click += DeleteButton_Click;
        _homeButton.Click += HomeButton_Click;
        _leftArrowButton.Click += LeftArrowButton_Click;
        _rightArrowButton.Click += RightArrowButton_Click;
        _endButton.Click += EndButton_Click;

        if (!String.IsNullOrEmpty(TabScope) && (FindName(TabScope) == null))
        {
            throw new InvalidOperationException(String.Format(
                "TouchKeyboard.TabScope property set to \"{0}\", but no such control exists", TabScope));
        }

        ClientUtil.ForEachDescendant(_layoutRoot, delegate(UIElement element)
        {
            var button = element as TouchKeyboardCharacterButton;
            if (button != null)
            {
                button.Click += TouchKeyboardCharacterButton_Click;
            }
        });

		EnableKeyboard(false);

        _focusPollTimer = new DispatcherTimer();
        _focusPollTimer.Interval = TimeSpan.FromMilliseconds(50);
        _focusPollTimer.Tick += new EventHandler(_focusPollTimer_Tick);
        _focusPollTimer.Start();
    }

    void _disabledVisualElement_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        // the user clicked the keyboard when it was disabled
        if (_isDisabledHelpDisplayed)
        {
			// help text is currently displayed -- hide it
            VisualStateManager.GoToState(this, HideDisabledHelpStateName, true);
			_isDisabledHelpDisplayed = false;
        }
		else
		{
        	// display a help text explaining the situation, wait, then hide it
			_isDisabledHelpDisplayed = true;
			ControlHelper.GoToVisualState(this, ShowDisabledHelpStateName, true, delegate
			{
				VisualStateManager.GoToState(this, HideDisabledHelpStateName, true);
				_isDisabledHelpDisplayed = false;
			});
		}
    }

    void _focusPollTimer_Tick(object sender, EventArgs e)
    {
		CheckTargetTextBox();
	}

	void CheckTargetTextBox()
	{
        object focusedElement = FocusManager.GetFocusedElement();
        var targetTextBox = focusedElement as TextBox;
        if ((targetTextBox != null) && (_targetTextBox == null))
        {
            // enable keyboard
            EnableKeyboard(true);
        }
        else
        if ((targetTextBox == null) && (_targetTextBox != null))
        {
            // disable keyboard
            EnableKeyboard(false);
        }
        _targetTextBox = targetTextBox;
    }

    void EnableKeyboard(bool enable)
    {
        if (enable)
        {
            VisualStateManager.GoToState(this, NormalStateName, true);
            VisualStateManager.GoToState(this, HideDisabledHelpStateName, false);
        }
        else
            VisualStateManager.GoToState(this, DisabledStateName, true);
    }

    void TouchKeyboardCharacterButton_Click(object sender, RoutedEventArgs e)
    {
        var button = sender as TouchKeyboardCharacterButton;
        //ControlHelper.Alert("Clicked TouchKeyboardCharacterButton: {0}", button.Characters);
		CheckTargetTextBox();
        if (_targetTextBox != null)
        {
            string chars = button.Characters;
            if (chars.Length == 1)
            {
                // keyboard button has one character on it, e.g. "Q" -- in this case, the
                // unshifted value is the lowercase version of the character and the shifted
                // value is the character itself; the Caps Lock key reverses this behavior
                if (_isShifted ^ _isCapsLocked)
                    InsertCharacter(chars[0]);
                else
                    InsertCharacter(char.ToLower(chars[0]));
            }
            else
            if (chars.Length == 2)
            {
                // keyboard button has two characters on it, e.g. "2@" -- in this case the first
                // character is the unshifted value and the second is the shifted value; the Caps
                // Lock key doesn't affect these kinds of keys
                if (_isShifted)
                    InsertCharacter(chars[1]);
                else
                    InsertCharacter(chars[0]);
            }
            else
                throw new Exception("Internal error");//TODO
        }
    }

    /// <summary>
    /// Called when the user clicks the Backspace button.
    /// </summary>
    ///
    /// <param name="sender">The object where the event handler is attached.</param>
    ///
    /// <param name="e">The event data.</param>
    ///
    private void BackspaceButton_Click(object sender, RoutedEventArgs e)
    {
		CheckTargetTextBox();
        if (_targetTextBox != null)
        {
            int index;
            if (_targetTextBox.SelectionLength > 0)
            {
                // the section is non-zero-length -- delete the selected text
                _targetTextBox.SelectedText = string.Empty;
            }
            else
            if ((index = _targetTextBox.SelectionStart) > 0)
            {
                // the selection is a zero-length insertion point past the start of the text --
                // delete the character before the insertion point
                string text = _targetTextBox.Text;
                _targetTextBox.Text = text.Substring(0, index - 1) + text.Substring(index);
                _targetTextBox.SelectionLength = 0;
                _targetTextBox.SelectionStart = index - 1;
            }
        }
    }

    /// <summary>
    /// Called when the user clicks the Tab button.
    /// </summary>
    ///
    /// <param name="sender">The object where the event handler is attached.</param>
    ///
    /// <param name="e">The event data.</param>
    ///
    private void TabButton_Click(object sender, RoutedEventArgs e)
    {
        // NOTE: Although TouchKeyboard works with any text box, Tab only tabs among
        // TouchTextBox instances (because we want to examine TabIndex on those, which is a
        // simplification)

        CheckTargetTextBox();

        if (_targetTextBox != null)
        {
            DependencyObject scope = Parent;
            if (scope == null)
                return;
            string tabScope = TabScope;
            while (true)
            {
                if (tabScope != null)
                {
                    var element = scope as FrameworkElement;
                    if ((element != null) && (element.Name == tabScope))
                        break;
                }

                DependencyObject parent = VisualTreeHelper.GetParent(scope);
                if (parent != null)
                    scope = parent;
                else
                    break;
            }

            var touchTextBoxes = new List<TouchTextBox>();
            SearchElementTree(scope, touchTextBoxes, 1, textBox => textBox.IsEnabled);

            if (touchTextBoxes.Count == 0)
                return;

            touchTextBoxes.Sort((x, y) => x.TabIndex - y.TabIndex);

            int index = 0;
            for (; index < touchTextBoxes.Count; index++)
            {
                if (Object.ReferenceEquals(touchTextBoxes[index].TextBox, _targetTextBox))
                    break;
            }

            if (_isShifted)
                index = (index + touchTextBoxes.Count - 1) % touchTextBoxes.Count;
            else
                index = (index + 1) % touchTextBoxes.Count;

            TouchTextBox newSelection = touchTextBoxes[index];
            newSelection.TextBox.SelectAll();
            newSelection.Focus();
        }
    }

	static void SearchVisualTree<T>(DependencyObject root, List<T> descendants, int depth, Predicate<T> predicate)
        where T : Control
	{
		int count = VisualTreeHelper.GetChildrenCount(root);
		for (int index = 0; index < count; index++)
		{
			DependencyObject child = VisualTreeHelper.GetChild(root, index);

            var element = child as FrameworkElement;

            T childAsT = child as T;
            if ((childAsT != null) && predicate(childAsT))
                descendants.Add(childAsT);

			SearchVisualTree(child, descendants, depth + 1, predicate);
		}
	}

    static void SearchElementTree<T>(DependencyObject root, List<T> descendants, int depth, Predicate<T> predicate)
        where T : Control
    {
        var panel = root as Panel;
        if (panel != null)
        {
            foreach (UIElement child in panel.Children)
            {
                var element = child as FrameworkElement;

                T childAsT = child as T;
                if ((childAsT != null) && predicate(childAsT))
                    descendants.Add(childAsT);

                SearchElementTree(child, descendants, depth + 1, predicate);
            }
        }

    }

    /// <summary>
    /// Called when the user clicks the Caps Lock button.
    /// </summary>
    ///
    /// <param name="sender">The object where the event handler is attached.</param>
    ///
    /// <param name="e">The event data.</param>
    ///
    void CapsLockButton_Click(object sender, RoutedEventArgs e)
    {
        _isCapsLocked = !_isCapsLocked;
        _capsLockButton.IsLit = _isCapsLocked;
    }

    /// <summary>
    /// Called when the user clicks the Enter button.
    /// </summary>
    ///
    /// <param name="sender">The object where the event handler is attached.</param>
    ///
    /// <param name="e">The event data.</param>
    ///
    void EnterButton_Click(object sender, RoutedEventArgs e)
    {
        if (EnterClick != null)
            EnterClick(this, new RoutedEventArgs());
    }

    /// <summary>
    /// Called when the user clicks either Shift button.
    /// </summary>
    ///
    /// <param name="sender">The object where the event handler is attached.</param>
    ///
    /// <param name="e">The event data.</param>
    ///
    void ShiftButton_Click(object sender, RoutedEventArgs e)
    {
        SetShiftState(!_isShifted);
    }

    /// <summary>
    /// Called when the user clicks the spacebar.
    /// </summary>
    ///
    /// <param name="sender">The object where the event handler is attached.</param>
    ///
    /// <param name="e">The event data.</param>
    ///
    private void SpaceButton_Click(object sender, RoutedEventArgs e)
    {
		CheckTargetTextBox();
        if (_targetTextBox != null)
			InsertCharacter(' ');
    }

    /// <summary>
    /// Called when the user clicks the Delete button.
    /// </summary>
    ///
    /// <param name="sender">The object where the event handler is attached.</param>
    ///
    /// <param name="e">The event data.</param>
    ///
    void DeleteButton_Click(object sender, RoutedEventArgs e)
    {
		// perform the delete operation, if a text box has focus
		CheckTargetTextBox();
        if (_targetTextBox != null)
		{
			if (_targetTextBox.SelectionLength > 0)
			{
				// the section is non-zero-length -- delete the selected text
				_targetTextBox.SelectedText = string.Empty;
			}
			else
			{
				// the selection is a zero-length insertion point
				string text = _targetTextBox.Text;
				int index = _targetTextBox.SelectionStart;
				if (index < text.Length)
				{
					// the insertion point is before the end of the text -- delete the character
					// after the insertion point
					_targetTextBox.Text = text.Substring(0, index) + text.Substring(index + 1);
					_targetTextBox.SelectionLength = 0;
					_targetTextBox.SelectionStart = index;
				}
			}
		}

		// clear the shift state
		SetShiftState(false);
    }

    /// <summary>
    /// Called when the user clicks the Home button.
    /// </summary>
    ///
    /// <param name="sender">The object where the event handler is attached.</param>
    ///
    /// <param name="e">The event data.</param>
    ///
    void HomeButton_Click(object sender, RoutedEventArgs e)
    {
		CheckTargetTextBox();
        if (_targetTextBox != null)
        {
            _targetTextBox.SelectionLength = 0;
            _targetTextBox.SelectionStart = 0;
        }
    }

    /// <summary>
    /// Called when the user clicks the left arrow button.
    /// </summary>
    ///
    /// <param name="sender">The object where the event handler is attached.</param>
    ///
    /// <param name="e">The event data.</param>
    ///
    void LeftArrowButton_Click(object sender, RoutedEventArgs e)
    {
		CheckTargetTextBox();
        if (_targetTextBox != null)
        {
            if (_isShifted)
            {
                // note: due to Silverlight limitations (specifically, inability to determine if
                // the insertion point is located at the beginning or end of the selection),
                // backward selection isn't supported here -- it would require unacceptable
                // trickery
                if (_targetTextBox.SelectionLength > 0)
                    _targetTextBox.SelectionLength--;
                else
                    _targetTextBox.SelectionStart--;
            }
            else
            {
                _targetTextBox.SelectionLength = 0;
                if (_targetTextBox.SelectionStart > 0)
                    _targetTextBox.SelectionStart--;
            }
        }
    }

    /// <summary>
    /// Called when the user clicks the right arrow button.
    /// </summary>
    ///
    /// <param name="sender">The object where the event handler is attached.</param>
    ///
    /// <param name="e">The event data.</param>
    ///
    void RightArrowButton_Click(object sender, RoutedEventArgs e)
    {
		CheckTargetTextBox();
        if (_targetTextBox != null)
        {
            if (_isShifted)
                _targetTextBox.SelectionLength++;
            else
            {
                int selectionLength = _targetTextBox.SelectionLength;
                _targetTextBox.SelectionLength = 0;
                _targetTextBox.SelectionStart += selectionLength + 1;
            }
        }
    }

    /// <summary>
    /// Called when the user clicks the End button.
    /// </summary>
    ///
    /// <param name="sender">The object where the event handler is attached.</param>
    ///
    /// <param name="e">The event data.</param>
    ///
    void EndButton_Click(object sender, RoutedEventArgs e)
    {
		CheckTargetTextBox();
        if (_targetTextBox != null)
        {
            if (_isShifted)
            {
                int index = _targetTextBox.SelectionStart;
                _targetTextBox.SelectAll();
                _targetTextBox.SelectionStart = index;
            }
            else
                _targetTextBox.SelectionStart = _targetTextBox.Text.Length;
        }
    }

    void SetShiftState(bool isShifted)
	{
		// do nothing if the current shift state is already the desired shift state
		if (_isShifted == isShifted)
			return;

        _isShifted = isShifted;

        _leftShiftButton.IsLit = isShifted;
        _rightShiftButton.IsLit = isShifted;

        // TODO: need TouchButton.Lit property; here, turn it on/off
        ClientUtil.ForEachDescendant(_layoutRoot, delegate(UIElement element)
        {
            var button = element as TouchKeyboardCharacterButton;
            if ((button != null) && (button.Characters.Length == 2))
            {
                if (isShifted)
                    VisualStateManager.GoToState(button, TouchKeyboardCharacterButton.ShiftedName, true);
                else
                    VisualStateManager.GoToState(button, TouchKeyboardCharacterButton.UnshiftedName, true);
            }
        });
    }

	void InsertCharacter(char ch)
	{
		_targetTextBox.SelectedText = new String(ch, 1);
		SetShiftState(false);
	}

    //////////////////////////////////////////////////////////////////////////////////////////////
    // IControlHelperConnector Implementation
    //

    Control IControlHelperConnector.GetControl()
    {
        return this;
    }

    DependencyObject IControlHelperConnector.GetTemplateChild(string childName)
    {
        return GetTemplateChild(childName);
    }
}

[TemplatePart(Name = InnerButtonName, Type = typeof(ButtonBase))]
[TemplatePart(Name = UnshiftedLabelName, Type = typeof(TextBlock))]
[TemplatePart(Name = ShiftedLabelName, Type = typeof(TextBlock))]
[TemplateVisualState(GroupName = ShiftStatesName, Name = UnshiftedName)]
[TemplateVisualState(GroupName = ShiftStatesName, Name = ShiftedName)]
[System.Diagnostics.DebuggerDisplay("TouchKeyboardCharacterButton {Characters}")]
public class TouchKeyboardCharacterButton : Control, IControlHelperConnector
{
    public const string InnerButtonName = "InnerButton";
    public const string UnshiftedLabelName = "UnshiftedLabel";
    public const string ShiftedLabelName = "ShiftedLabel";

    public const string ShiftStatesName = "ShiftStates";
    public const string UnshiftedName = "Unshifted";
    public const string ShiftedName = "Shifted";

    ButtonBase _innerButton;
    TextBlock _unshiftedLabel;
    TextBlock _shiftedLabel;

    public static readonly DependencyProperty CharactersProperty =
        DependencyProperty.Register("Characters", typeof(string), typeof(TouchKeyboardCharacterButton), null);

    public event RoutedEventHandler Click;

    public TouchKeyboardCharacterButton()
    {
        base.DefaultStyleKey = typeof(TouchKeyboardCharacterButton);
    }

    public string Characters
    {
        get
        {
            return (string)GetValue(CharactersProperty);
        }
        set
        {
            SetValue(CharactersProperty, value);
        }
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        ControlHelper.GetRequiredTemplateChild(this, InnerButtonName, out _innerButton);
        ControlHelper.GetRequiredTemplateChild(this, UnshiftedLabelName, out _unshiftedLabel);
        ControlHelper.GetRequiredTemplateChild(this, ShiftedLabelName, out _shiftedLabel);

        string chars = Characters;
        if (chars.Length == 1)
        {
            _unshiftedLabel.Text = " ";
            _shiftedLabel.Text = chars.Substring(0, 1);
            VisualStateManager.GoToState(this, ShiftedName, false);
        }
        else
        if (chars.Length == 2)
        {
            _unshiftedLabel.Text = chars.Substring(0, 1);
            _shiftedLabel.Text = chars.Substring(1, 1);
        }

        _innerButton.Click += _innerButton_Click;
    }

    void _innerButton_Click(object sender, RoutedEventArgs e)
    {
        if (Click != null)
            Click(this, new RoutedEventArgs());
    }

    //////////////////////////////////////////////////////////////////////////////////////////////
    // IControlHelperConnector Implementation
    //

    Control IControlHelperConnector.GetControl()
    {
        return this;
    }

    DependencyObject IControlHelperConnector.GetTemplateChild(string childName)
    {
        return GetTemplateChild(childName);
    }
}

}
